
Alkaline.Modules.newModule('Minify', function() {

this.requiredModules = [{ name: 'Analyzer' }];

var TOKEN = Alkaline.JSInfo.TOKEN;
var tokenNames = Alkaline.JSInfo.tokenNames;

/**
 *	import classes from Analyzer module
 */
var LexicalContext = Alkaline.Processing.LexicalContext;
var VariableDefinition = Alkaline.Processing.VariableDefinition;

/**
 *	run ( ast )
 */
this.run = function(ast) {
	this.builder = this.compileResult.treeBuilder;
	this.comments = [];
	this.functionNodes = [];
	this.skipRename = [];
	this.DFS(ast, [ TOKEN.Function, TOKEN.CALL, TOKEN.With, TOKEN.Operator, TOKEN.Comment ], this.process);
	for (var i = 0; i < this.comments.length; i++)
		// remove comments but only if the first character is not "!"
		if (this.comments[i].token.text.charAt(2) !== '!')
			this.builder.removeTree(this.comments[i]);
	this.minifyBLOCK(ast);
	for (var i = 0; i < this.functionNodes.length; i++)
		this.minifyFunction(this.functionNodes[i], this.skipRename[i]);
}

this.blackListTree = function(ast) {
	while (ast) {
		var pos = this.functionNodes.indexOf(ast);
		if (pos > -1)
			this.skipRename[pos] = true;
		ast = ast.parent;
	}
}

var treeIsDirectlyInsideFunction = function(tree, func) {
	while (tree && tree.token.type !== TOKEN.Function)
		tree = tree.parent;
	return tree === func;
}

this.minifyBLOCK = function(ast) {
	this.minifyVarDecl(ast, ast.globalContext);
}

this.minifyFunction = function(funAST, skipRename) {
	var fun = funAST.objectValue;
	var ctx = fun.innerLexicalContext;
	
	if (!skipRename) {
		this.removeFunctionArgumentsIfNotUsed(fun);
		var vars = ctx.getLocalVariables();
		// sort descending:
		vars.sort(function(a, b) {
			return (b.referencedFrom.length + !!b.declarationToken) - (a.referencedFrom.length + !!a.declarationToken);
		});
		this.removeUnusedVariablesInList(vars, ctx);
		this.minimizeVariableNames(vars, ctx);
	}
	
	this.minifyVarDecl(funAST, ctx);
}

this.minifyVarDecl = function(ast, ctx) {
	var vars = ctx.getLocalVariables();
	vars = this.getExplicitVariables(vars);
	this.mergeVariableInitializations(vars);
	vars = this.getUninitializedVariables(vars);
	this.insertVarDeclarations(vars, ast);
}

this.removeFunctionArgumentsIfNotUsed = function(fun) {
	var ctx = fun.innerLexicalContext;
	// remove arguments which are not used from the end of the arg list
	var args = fun.argumentsTree.children || [], obj;
	for (var i = args.length-1; i >= 0 && !(obj = ctx.getVariable(args[i].token.text)).referencedFrom.length; i--)
		// remove the argument from the function
		ctx.removeVariable(obj);
}

this.removeUnusedVariablesInList = function(list, ctx) {
	for (var i = 0; i < list.length; i++)
		if (list[i].declarationToken && list[i].sourceType === VariableDefinition.SOURCE_TYPE.DECLARED_VAR)
			if (!list[i].referencedFrom.length && list[i].declarationToken && list[i].declarationToken.parent.token.type === TOKEN.Var) {
				ctx.removeVariable(list[i]);
				list.splice(i, 1);
				i--;
			}
}

this.minimizeVariableNames = function(list, context) {
	var bottomContexts = context.findAllBottomLevelContexts();
	var nextName = LexicalContext.getNewVariableNameAvailableInContexts(bottomContexts);
	var lastLength = 1;
	
	for (var i = 0; i < list.length; i++) {
		if (!list[i].declarationToken) {
			// can't rename this (e.g. "arguments")
			continue;
		}
		var varObj = list[i];
		
		if (varObj.name.length > nextName.length) {
			// rename the variable
			varObj.renameTo(nextName);
			nextName = LexicalContext.getNewVariableNameAvailableInContexts(bottomContexts);
		}
		
		if (varObj.name.length > lastLength)
			for (var j = i + 1; j < list.length; j++)
				if (list[j].name.length < varObj.name.length) {
					var newName = list[j].name;
					var oldName = varObj.name;
					list[j].renameTo(nextName);
					varObj.renameTo(newName);
					list[j].renameTo(oldName);
				}
		
		lastLength = varObj.name.length;
	}
}

this.getExplicitVariables = function(list) {
	var explicit = [];
	for (var i = 0; i < list.length; i++)
		if (list[i].sourceType === VariableDefinition.SOURCE_TYPE.DECLARED_VAR)
			explicit.push(list[i]);
	return explicit;
}

this.mergeVariableInitializations = function(list) {
	if (!list.length) return;
	var varDecl = list[0].declarationToken.parent;
	var parent = varDecl.parent;
	do {
		var repeat = false;
		if (varDecl.childIndex < parent.children.length-1) {
			var nextSibling = parent.children[varDecl.childIndex+1];
			if (nextSibling.token.type === TOKEN.EXPR && nextSibling.children && nextSibling.children.length)
				nextSibling = nextSibling.children[0];
			if (nextSibling.token.type === TOKEN.Operator && nextSibling.token.text === "=" && nextSibling.children[0].token.type === TOKEN.Id) {
				var varRef = nextSibling.children[0].objectValue;
				var idx = list.indexOf(varRef);
				if (idx > -1) {
					list.splice(idx, 1);
					this.builder.moveTreeToBeforeEndOfParent(varRef.declarationToken);
					this.builder.addChild(varRef.declarationToken, nextSibling.children[1]);
					this.builder.removeTree(nextSibling);
					repeat = true;
				}
			}
		}
	} while (repeat);
}

this.getUninitializedVariables = function(list) {
	var newList = [];
	for (var i = 0; i < list.length; i++)
		if (list[i].declarationToken && !(list[i].declarationToken.children && list[i].declarationToken.children.length))
			newList.push(list[i]);
	return newList;
}

this.insertVarDeclarations = function(list, funAST) {
	var lastVarCandidate = null;
	nextVariable:
	for (var i = 0; i < list.length; i++)
		// search for a reference directly inside this function, where it is assigned (top-level or inside a for)
		nextReference:
		for (var refI = 0; refI < list[i].referencedFrom.length; refI++) {
			var assgn = list[i].referencedFrom[refI].parent;
			if (funAST.token.type === TOKEN.Function && !treeIsDirectlyInsideFunction(assgn, funAST))
				continue;
			var acceptAlone = list[i].name.length >= 3 || list.length === 1;
			if (assgn.token.text === "=" && list[i].referencedFrom[refI].childIndex == 0) {
				if (Alkaline.JSInfo.expressionCouldBeStatement(assgn) || (assgn.parent.token.type === TOKEN.For && assgn.childIndex === 0))
					// X = ...
					// for (X = ... ; ;)
					if (acceptAlone) {
						lastVarCandidate = this.assignmentToVarDecl(assgn, list);
						continue nextVariable;
					} else
						lastVarCandidate = assgn;
				else if (assgn.parent.token.type === TOKEN.EXPR)
					if (Alkaline.JSInfo.expressionCouldBeStatement(assgn.parent)) {
						// ... , X = ... , ...
						var expr = assgn.parent, children = expr.children;
						var start = assgn.childIndex;
						var stop = assgn.childIndex+1;
						while (start > 0) {
							var other = children[start-1];
							if (other.token.text === '=' && other.children[0].token.type === TOKEN.Id && list.contains(other.children[0].objectValue))
								start--;
							else
								break;
						}
						while (stop < children.length) {
							var other = children[stop];
							if (other.token.text === '=' && other.children[0].token.type === TOKEN.Id && list.contains(other.children[0].objectValue))
								stop++;
							else
								break;
						}
						if (acceptAlone || stop - start > 1) {
							var newAssgn = this.sliceEXPR(expr, start, stop);
							lastVarCandidate = this.assignmentToVarDecl(newAssgn, list);
							continue nextVariable;
						} else
							lastVarCandidate = assgn;
					} else if (assgn.parent.parent.token.type === TOKEN.For && assgn.parent.childIndex === 0) {
						// for (... , X = ... , ... ; ; )
						var expr = assgn.parent;
						for (var j = 0; j < expr.children.length; j++)
							if (!list.contains(expr.children[j].children[0].objectValue))
								// we can't insert a variable declaration here because it would re-declare variables from the parent context
								continue nextReference;
						if (acceptAlone || expr.children.length > 1) {
							lastVarCandidate = this.assignmentToVarDecl(expr, list);
							continue nextVariable;
						} else
							lastVarCandidate = expr;
					}
			} else if (acceptAlone && assgn.token.text === "in" && assgn.parent.token.type === TOKEN.For && assgn.childIndex === 0 && list[i].referencedFrom[refI].childIndex == 0) {
					// for (X in ...)
				var newDecl = this.builder.Var();
				this.builder.replaceTree(list[i].referencedFrom[refI], newDecl);
				newDecl.addChild(this.builder.Id(list[i].name));
				if (list[i].declarationToken) {
					this.builder.removeTree(list[i].declarationToken);
					list[i].declarationToken = null;
				}
				list.splice(i, 1);
				i--;
				continue nextVariable;
			}
		}
	if (lastVarCandidate && list.length) {
		var varDecl;
		if (lastVarCandidate.token.type === TOKEN.Var)
			varDecl = lastVarCandidate;
		else {
			if (lastVarCandidate.parent.token.type === TOKEN.EXPR)
				lastVarCandidate = this.sliceEXPR(lastVarCandidate.parent, lastVarCandidate.childIndex, lastVarCandidate.childIndex+1);
			varDecl = this.assignmentToVarDecl(lastVarCandidate, list);
		}
		for (var i = 0; i < list.length; i++) {
			this.builder.addChild(varDecl, this.builder.Id(list[i].name));
			if (list[i].declarationToken) {
				this.builder.removeTree(list[i].declarationToken);
				list[i].declarationToken = null;
			}
		}
	}
}

this.sliceEXPR = function(expr, start, stop) {
	if (expr.parent.token.type !== TOKEN.BLOCK)
		// for (...) ... , X = ... , ...
		// while (...) ... , X = ... , ...
		// etc.
		expr.parent.setChild(expr.childIndex, this.builder.BLOCK([expr]));
	var block = expr.parent;
	var children = expr.children;
	var post;
	if (stop < children.length)
		post = this.builder.EXPR(children.splice(stop));
	var newAssgn = this.builder.EXPR(children.splice(start));
	var start = expr.childIndex + (start ? 1 : 0);
	// this will remove the old EXPR if there are no operations before the first assignment spliced, and append after it otherwise:
	block.replaceChildren(start, expr.childIndex, newAssgn);
	if (post)
		block.replaceChildren(start+1, start, post);
	return newAssgn;
}

this.assignmentToVarDecl = function(assgn, list) {
	var newDecl = this.builder.Var();
	var oThis = this;
	
	var importInitialization = function(assgn) {
		var id = assgn.children[0];
		var idx = list.indexOf(id.objectValue);
		if (idx >= 0) {
			if (list[idx].declarationToken) {
				oThis.builder.removeTree(list[idx].declarationToken);
				list[idx].declarationToken = null;
			}
			list.splice(idx, 1);
		}
		newDecl.addChild(id);
		id.addChild(assgn.children[1]);
	}
	
	if (assgn.token.type === TOKEN.EXPR)
		for (var i = 0; i < assgn.children.length; i++)
			importInitialization(assgn.children[i]);
	else
		importInitialization(assgn);
	this.builder.replaceTree(assgn, newDecl);
	
	return newDecl;
}

/**
 *	AST processing
 */
this.process = function(ast) {
	var ret = this['process' + ast.token.type](ast);
}

this.processCALL = function(ast) {
	if (ast.children.length > 1 && ast.children[0].token.type === TOKEN.Id && ast.children[0].token.text === "eval")
		this.blackListTree(ast);
}

this.processComment = function(ast) {
	this.comments.push(ast);
}

this.processWith = function(ast) {
	this.blackListTree(ast);
}

this.processFunction = function(ast) {
	this.functionNodes.push(ast);
	this.skipRename.push(false);
}

this.processOperator = function(ast) {
	if (ast.token.text === "=" && ast.children[1].token.type === TOKEN.Operator
		&& Alkaline.JSInfo.isCollapsibleOperator(ast.children[1].token.text)
		&& this.builder.treesMatch(ast.children[0], ast.children[1].children[0])) {
		ast.token.text = ast.children[1].token.text + '=';
		this.builder.replaceTree(ast.children[1], ast.children[1].children[1]);
	}
}

for (var i = 0; i < tokenNames.length; i++)
	if (this['process' + tokenNames[i]])
		this['process' + TOKEN[tokenNames[i]]] = this['process' + tokenNames[i]];

});
